#!/usr/bin/env python
######################################################################
##
## Copyright (C) 2008,  Simon Kagstrom
##
## Filename:      shlcov
## Author:        Simon Kagstrom <simon.kagstrom@gmail.com>
## Description:   Output HTML for shell script coverage
##
## $Id:$
##
######################################################################

import os, sys, time, re, getopt, shutil

base_dir = os.path.abspath(sys.path[0] + "/../")
sys.path =  [base_dir] + sys.path

import shcov, shcov.config
from shcov.file import File
from shcov.utils import *

# Regexp to match functions
fn_regexp = re.compile("\A(function[\t, ]+)*[a-z,A-Z,_]+[a-z,A-Z,_,0-9]*[\t, ]*\([\t, ]*\)[\t, ]*")
brace_regexp = re.compile("\A(\{|\}){1}")

else_then_done_regexp = re.compile("\A(else|then|fi|done|esac|do|;;){1}\\b")
case_nr_regexp = re.compile("\A(([\',\",0-9,a-z,A-Z,_, ]+)|\*{1})\){1}\\Z")

class ShcovDataOutput:
    def __init__(self, script_base, inpath, outpath, low_limit = 15, high_limit = 50):
        self.script_base = script_base
        self.inpath = inpath
        self.outpath = outpath

        self.low_limit = low_limit
        self.high_limit = high_limit

        self.files = []

        # Setup the data path (helper files)
        try:
            self.data_path = base_dir + "/data"
            os.lstat(self.data_path)
        except:
            # No such dir, fall back to /usr/share
            self.data_path = "/usr/share/" + shcov.config.PROGRAM_NAME.lower() + "/data"

    def line_is_code(self, line):
        tmp = line.strip()
        if tmp.startswith('#'):
            return False
        # Functions and braces
        if tmp.startswith("function") or fn_regexp.match(tmp) or brace_regexp.match(tmp):
            return False
        # fi/else/esac etc
        if else_then_done_regexp.match(tmp):
            return False
        if case_nr_regexp.match(tmp):
            return False
        # Empty string
        if tmp == "":
            return False
        return True

    def calculate_statistics(self, file):
        source_data = read_file( file.get_source_path() )

        # OK, this is ugly: Just put these in the object...
        file.total_lines = 0
        file.executed_lines = 0

        nr = 1
        previousLineIsContinued = False
        for line in source_data.splitlines():
            if self.line_is_code(line):
                file.total_lines = file.total_lines + 1
                if file.lines.has_key(nr) or previousLineIsContinued == True:
                    file.executed_lines = file.executed_lines + 1

            if line.endswith("\\"):
                previousLineIsContinued = True
            else:
                previousLineIsContinued = False

            nr = nr + 1

    def get_relative_path_to_helpers(self, path):
        abs_outpath = os.path.abspath(self.outpath)

        path = path.replace(abs_outpath, '')
        path.replace('//', '/')

        return "../" * path.count('/')

    def write_header(self, of, basename, path_to_helpers, total_lines, executed_lines, is_directory = True):

        # No div-by-zero please
        if total_lines == 0:
            total_lines = 1

        # Taken from lcov
        header="""<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">

<html lang=\"en\">

<head>
  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=ISO-8859-1\">
  <title>SHCOV - %s</title>
  <link rel=\"stylesheet\" type=\"text/css\" href=\"%sgcov.css\">
</head>

<body>
""" % (basename, path_to_helpers)
        legend_file="""        <tr>
          <td class="legendItem" width="20%">Legend:</td>
          <td class="legendValue" width="80%" colspan=4>
            <span class="legendNoCov">
              not executed
            </span>
            <span class="legendCov">
              executed
            </span>
         </td>
        </tr>
"""
        legend_directory="""        <tr>
          <td class="legendItem" width="20%%">Legend:</td>
          <td class="legendValue" width="80%%" colspan=4>
            <span class="coverLegendLo">
              <b>Low:</b> 0%% to %d%%
            </span>
            <span class="coverLegendMed">
              <b>Medium:</b> %d%% to %d%%
            </span>
            <span class="coverLegendHi">
              <b>High:</b> %d%% to 100%%
            </span>
         </td>
       </tr>
""" % (self.low_limit, self.low_limit, self.high_limit, self.high_limit)
        legend = legend_file
        if is_directory:
            legend = legend_directory
        top_table="""  <table width="100%%" border=0 cellspacing=0 cellpadding=0>
    <tr><td class="title">SHCOV by Simon K&aring;gstr&ouml;m</td></tr>
    <tr><td class="ruler"><img src="%sglass.png" width=3 height=3 alt=""></td></tr>

    <tr>
      <td width="100%%">
        <table cellpadding=1 border=0 width="100%%">
        <tr>
          <td class="headerItem" width="20%%">Current&nbsp;view:</td>
          <td class="headerValue" width="80%%" colspan=4>%s</td>
        </tr>
        <tr>
          <td class="headerItem" width="20%%">Date:</td>
          <td class="headerValue" width="20%%">%s</td>
          <td width="20%%"></td>
          <td class="headerItem" width="20%%">Instrumented&nbsp;lines:</td>
          <td class="headerValue" width="20%%">%d</td>
        </tr>
        <tr>
          <td class="headerItem" width="20%%">Code&nbsp;covered:</td>
          <td class="headerValue" width="20%%">%.1f %%</td>
          <td width="20%%"></td>
          <td class="headerItem" width="20%%">Executed&nbsp;lines:</td>
          <td class="headerValue" width="20%%">%d</td>
        </tr>
        %s
        </table>
      </td>
    </tr>
    <tr><td class="ruler"><img src="%sglass.png" width=3 height=3 alt=""></td></tr>
  </table>
""" % (path_to_helpers, basename, time.strftime("%Y-%m-%d"),
       total_lines, (float(executed_lines) / float(total_lines)) * 100,
       executed_lines, legend, path_to_helpers)

        of.write(header)

        of.write(top_table)


    def write_footer(self, of):
        of.write("""</body>
</html>""")


    def handle_one_file(self, file):
        dirname = os.path.dirname(file.path)
        basename = os.path.basename(file.path)
        path = self.outpath + "/root/" + os.path.abspath(dirname)
        path = path.replace('//', '/')

        try:
            os.makedirs( path )
        except OSError, e:
            # This is OK (already exists)
            pass

        of = open(os.path.join(path, basename + ".html"), "w")
        path_to_helpers = self.get_relative_path_to_helpers(os.path.abspath(path) )

        source_data = read_file(self.script_base + file.path)

        table="""  <table cellpadding=0 cellspacing=0 border=0>
    <tr>
      <td><br></td>
    </tr>
    <tr>
      <td><pre class="source">
"""

        table_end="""</pre>
      </td>
    </tr>
  </table>
  <br>
"""
        self.write_header(of, basename, path_to_helpers,
                          file.total_lines, file.executed_lines, False)

        of.write(table)
        nr = 1
        previousLineIsContinued = False
        firstLineOfPossibleMultiLineBlock = nr
        for line in source_data.splitlines():
            line.replace(">", "&gt;")
            line.replace("<", "&lt;")
            line.replace('"', '&quot;')

            if file.lines.has_key(nr) and self.line_is_code(line) or nr>1 and previousLineIsContinued == True and file.lines.has_key(firstLineOfPossibleMultiLineBlock):
                if previousLineIsContinued == True:
                    of.write("""<span class="lineNum">     %5d</span><span class="lineCov">                :  %s</span>\n""" %
                             (nr, line))
                else:
                    of.write("""<span class="lineNum">     %5d</span><span class="lineCov">         %6d :  %s</span>\n""" %
                             (nr, file.lines[nr], line))
            else:
                if self.line_is_code(line):
                    of.write("""<span class="lineNum">     %5d</span><span class="lineNoCov">              0 :  %s</span>\n""" %
                             (nr, line))
                else:
                    of.write("""<span class="lineNum">     %5d</span>                :  %s\n""" % (nr, line))

            if line.endswith("\\"):
                previousLineIsContinued = True
            else:
                previousLineIsContinued = False

            nr = nr + 1

            if previousLineIsContinued == False:
                firstLineOfPossibleMultiLineBlock = nr

        of.write(table_end)

        self.write_footer(of)

    def write_directory_header(self, of):
        of.write("""<center>
  <table width="80%" cellpadding=2 cellspacing=1 border=0>

    <tr>
      <td width="50%"><br></td>
      <td width="15%"></td>
      <td width="15%"></td>
      <td width="20%"></td>
    </tr>

    <tr>
      <td class="tableHead">Directory&nbsp;name</td>
      <td class="tableHead" colspan=3>Coverage</td>
    </tr>
""")

    def write_directory_line(self, of, path_to_helpers, linkname, name, total_lines, executed_lines):
        fraction = (float(executed_lines) / float(total_lines)) * 100

        # Setup some data depending on the coverage
        covernum = "coverNumHi"
        covertype = "coverPerHi"
        color = "emerald"
        if fraction < self.high_limit:
            covernum = "coverNumMed"
            covertype = "coverPerMed"
            color = "amber"
        if fraction < self.low_limit:
            covernum = "coverNumLo"
            covertype = "coverPerLo"
            color = "ruby"

        of.write("""    <tr>
      <td class="coverFile"><a href="%s">%s</a></td>
      <td class="coverBar" align="center">
        <table border=0 cellspacing=0 cellpadding=1><tr><td class="coverBarOutline"><img src="%s%s.png" width=%d height=10 alt="%.1f%%"><img src="%ssnow.png" width=%d height=10 alt="%1f%%"></td></tr></table>
      </td>
      <td class="%s">%.1f&nbsp;%%</td>
      <td class="%s">%d&nbsp;/&nbsp;%d&nbsp;lines</td>
    </tr>
""" % (linkname, name, path_to_helpers, color,
       fraction, fraction, path_to_helpers, 100-fraction, fraction, covertype, fraction, covernum, executed_lines, total_lines) )


    def handle_files(self):
        "Output all shcov data"

        class Dir:
            def __init__(self, name):
                self.name = name
                self.basename = os.path.basename(name)
                self.total_lines = 0
                self.executed_lines = 0
                self.files = []

            def add_file(self, file):
                self.total_lines = self.total_lines + file.total_lines
                self.executed_lines = self.executed_lines + file.executed_lines
                self.files.append(file)

        dirs = {}

        for file in self.files:
            self.calculate_statistics(file)

        # Create dirs
        for file in self.files:
            dirname = os.path.dirname(file.path)

            try:
                dir = dirs[dirname]
            except KeyError, e:
                dir = Dir(dirname)
                dirs[dirname] = dir
            dir.add_file(file)

        total_lines = 0
        executed_lines = 0

        for dir in dirs.values():
            total_lines = total_lines + dir.total_lines
            executed_lines = executed_lines + dir.executed_lines

        of = open(os.path.join(self.outpath, "index.html"), "w")
        self.write_header(of, "/", "", total_lines, executed_lines)

        self.write_directory_header(of)

        for dir in dirs.values():
            path = "root/" + dir.name[1:] + "/index.html"
            path = path.replace('//', '/')

            self.write_directory_line(of, "", path, dir.name,
                                      dir.total_lines, dir.executed_lines)

        self.write_footer(of)
        of.close()

        # Create indices in each dir
        for dir in dirs.values():
            path = self.outpath + "/root/" + dir.name
            path = path.replace('//', '/')

            path_to_helpers = self.get_relative_path_to_helpers( os.path.abspath(path) )

            try:
                os.makedirs( path )
            except OSError, e:
                pass

            of = open(os.path.join(path, "index.html"), "w")

            self.write_header(of, dir.basename, path_to_helpers, dir.total_lines, dir.executed_lines)
            self.write_directory_header(of)
            for file in dir.files:
                self.write_directory_line(of, path_to_helpers, file.basename + ".html", file.basename,
                                        file.total_lines, file.executed_lines)
            self.write_footer(of)
            of.close()

        # Create the files one by one
        for file in self.files:
            self.handle_one_file(file)


    def run(self):
        try:
            os.makedirs( self.outpath )
        except OSError, e:
            pass
        file_objs = {} # Map names to objs

        for root, dirs, files in os.walk(self.inpath, topdown=False):
            for name in files:
                path = os.path.join(root, name)
                try:
                    file = shcov.file.load(path, script_base = self.script_base)
                    if file_objs.has_key(file.path):
                        other = file_objs[file.path]

                        # If the files are the same, merge them
                        if file.digest == other.digest:
                            other.merge_object(file)
                            continue
                        else:
                            # Not the same, replace and take the newest
                            if file.get_source_ctime() > other.get_source_ctime():
                                file_objs[file.path] = file
                                self.files.remove(other)

                    file_objs[file.path] = file
                    self.files.append(file)
                except Exception, e:
                    print "Could not load", path, "ignoring"

        self.handle_files()

        # And copy all the data
        for root, dirs, files in os.walk(self.data_path, topdown=False):
            for name in files:
                path = os.path.join(root, name)
                shutil.copyfile(path, os.path.join(self.outpath, name))

def usage():
    print "Usage: shlcov [--script-base=path] [--limit=low,high] datadir outdir\n"
    print "Create HTML output of shcov data in 'datadir' in 'outdir'.\n"
    print "Options are"
    print "  --script-base=path   set the base path to lookup script source (default '')"
    print "  --limit=low,high     set the low and high coverage limits (default 15,50)"

    sys.exit(1)

if __name__ == "__main__":
    script_base = ''
    low_limit = 15
    high_limit = 50

    try:
        optlist, args = getopt.gnu_getopt(sys.argv[1:], "hs:l:", ["help", "script-base=", "limit="])
    except:
        usage()

    for opt, arg in optlist:
        if opt in ("-h", "--help"):
            usage()
        if opt in ("-s", "--script-base"):
            script_base = arg
        if opt in ("-l", "--limit"):
            lim = arg.split(",")
            if len(lim) != 2:
                usage()
            try:
                low_limit = int(lim[0])
                high_limit = int(lim[1])
            except:
                usage()

    if len(args) < 2:
        usage()

    sc = ShcovDataOutput(script_base, args[0], args[1], low_limit, high_limit)
    sc.run()
